/**
 * 
 */
package adnotatio.rdf;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import adnotatio.common.xml.XMLDocument;
import adnotatio.common.xml.XMLElement;
import adnotatio.common.xml.XMLName;
import adnotatio.common.xml.XMLNode;
import adnotatio.common.xml.XMLText;

/**
 * This is a simple RDF parser transforming the given XML DOM objects in RDF
 * resources.
 * 
 * @author kotelnikov
 */
class RDFParser implements BasicVocabulary {

    /**
     * Handler instances are used to handle in a specific way various types of
     * XML nodes.
     * 
     * @author kotelnikov
     */
    interface IElementHandler {

        /**
         * This method should handle the given XML element and make appropriate
         * modifications in the model.
         * 
         * @param e the node to handle
         */
        void handle(XMLElement e);
    }

    /**
     * This handler expects an XML node corresponding to an RDF resource.
     * 
     * @author kotelnikov
     */
    class ResourceHandler implements IElementHandler {

        /**
         * This method interprets and sets the content of the given XML element
         * as a resource property.
         * 
         * @param resource the resource to which the property should be added
         * @param propertyID the identifier of the property to add
         * @param propertyElement the XML element defining a property
         */
        private void addResourceProperty(
            Resource resource,
            ID propertyID,
            XMLElement propertyElement) {
            Object result = null;
            String parseType = propertyElement
                .getAttributeValue(ATTR_RDF_PARSE_TYPE);
            if ("Collection".equals(parseType)) {
                result = handleContentAsContainer(propertyElement);
            } else if ("Resource".equals(parseType)) {
                // The value of the property is an intermediate resource and
                // all child XML elements define properties of this intermediate
                // resource.
                result = handleContentAsResource(propertyElement);
            } else {
                // All child XML nodes are interpreted as new resources or as
                // a simple literal value (as a string) if no XML elements was
                // found
                result = handleContentAsLiteralOrResources(propertyElement);
            }
            resource.addValue(propertyID, result);
        }

        /**
         * Adds the value of a property corresponding to the given XML name to
         * the resource.
         * 
         * @param resource to this resource a new property will be added.
         * @param name the name of the property to add
         * @param value the value to add
         */
        private void addResourceProperty(
            Resource resource,
            XMLName name,
            String value) {
            ID propertyID = toID(name);
            if (!RDF_ID.equals(propertyID) && !RDF_ABOUT.equals(propertyID)) {
                resource.addValue(propertyID, value);
            }
        }

        /**
         * Returns the specified attribute of the XML element as an identifier.
         * 
         * @param e the XML element
         * @param attrName the name of the attribute to interpret as an
         *        identifier
         * @return the specified attribute of the XML element as an identifier
         */
        private ID getAttributeAsId(XMLElement e, XMLName attrName) {
            String str = e.getAttributeValue(attrName);
            return (str != null) ? new ID(str) : null;
        }

        /**
         * Returns an identifier for the resource corresponding to the given XML
         * element. If the given element does not define "rdf:about" or "rdf:ID"
         * attributes then this
         * 
         * @param e the XML element defining the resource
         * @return an identifier for the resource defined by the given XML
         *         element
         */
        private ID getResourceID(XMLElement e) {
            ID result = getAttributeAsId(e, ATTR_RDF_ID);
            if (result == null) {
                result = getAttributeAsId(e, ATTR_RDF_ABOUT);
            }
            if (result == null) {
                result = new ID();
            }
            return result;
        }

        /**
         * This method identify and return the type of a resource defined by the
         * given XML element
         * 
         * @param e the XML element defining a resource
         * @return the type of the resource defined by the given XML node
         */
        private ID getResourceTypeID(XMLElement e) {
            ID typeId = toID(e);
            return RDF_DESCRIPTION.equals(typeId) ? null : typeId;
        }

        /**
         * This method interprets children of the given XML element as resource
         * descriptions
         * 
         * @see adnotatio.rdf.RDFParser.IElementHandler#handle(adnotatio.common.xml.Element)
         */
        public void handle(XMLElement e) {
            int len = e.getChildNumber();
            for (int i = 0; i < len; i++) {
                XMLNode child = e.getChild(i);
                if (!(child instanceof XMLElement))
                    continue;
                XMLElement childElement = (XMLElement) child;
                handleResourceElement(childElement);
            }
        }

        /**
         * This method interprets the content of the given XML element as
         * resource properties and adds these properties to the specified
         * resource.
         * 
         * @param e the XML element defining properties of the resource
         * @param resource the resource for which various properties should be
         *        added
         */
        private void handleChildrenOfResourceElement(
            XMLElement e,
            Resource resource) {
            int len = e.getChildNumber();
            for (int i = 0; i < len; i++) {
                XMLNode node = e.getChild(i);
                if (!(node instanceof XMLElement))
                    continue;
                XMLElement propertyElement = (XMLElement) node;
                ID propertyID = toID(propertyElement);
                addResourceProperty(resource, propertyID, propertyElement);
            }
        }

        /**
         * This method creates a new container and adds all resources encoded in
         * the given element as a list of resources.
         * 
         * @param e the element containing a list of resources
         * @return a new container with attached resources
         */
        private Container handleContentAsContainer(XMLElement e) {
            Container container = fModel.newContainer();
            // All XML elements are interpreted as resource descriptions
            int len = e.getChildNumber();
            for (int i = 0; i < len; i++) {
                XMLNode child = e.getChild(i);
                if (!(child instanceof XMLElement))
                    continue;
                XMLElement childElement = (XMLElement) child;
                ID typeID = toID(childElement);
                Resource target = null;
                if (RDF_LI.equals(typeID)) {
                    target = handleContentAsResource(childElement);
                } else {
                    target = handleResourceElement(childElement);
                }
                container.add(target);
            }
            return container;
        }

        /**
         * This method interprets the content of the given XML element as a
         * literal value or as a sequence of resources
         * 
         * @param e the element defining the value of properties
         * @return
         */
        private Object handleContentAsLiteralOrResources(XMLElement e) {
            Object result;
            // All child XML nodes are interpreted as new resources or as
            // a simple literal value (as a string) if no XML elements was
            // found
            ID targetID = getAttributeAsId(e, ATTR_RDF_RESOURCE);
            if (targetID != null) {
                result = fModel.getResource(targetID, true);
            } else {
                StringBuffer buf = new StringBuffer();
                int len = e.getChildNumber();
                List list = new ArrayList();
                for (int i = 0; i < len; i++) {
                    XMLNode child = e.getChild(i);
                    if (child instanceof XMLElement) {
                        XMLElement childElement = (XMLElement) child;
                        ID typeID = getResourceTypeID(childElement);
                        if (RDF_SEQ.equals(typeID)
                            || RDF_ALT.equals(typeID)
                            || RDF_BAG.equals(typeID)) {
                            Container container = handleContentAsContainer(childElement);
                            list.add(container);
                        } else {
                            Resource target = handleResourceElement(childElement);
                            list.add(target);
                        }
                    } else if (list.size() == 0 && child instanceof XMLText) {
                        XMLText d = (XMLText) child;
                        buf.append(d.getText());
                    }
                }
                if (list.isEmpty()) {
                    result = buf.toString();
                } else if (list.size() == 1) {
                    result = list.get(0);
                } else {
                    Container container = fModel.newContainer();
                    for (Iterator iterator = list.iterator(); iterator
                        .hasNext();) {
                        Object value = iterator.next();
                        container.add(value);
                    }
                    result = container;
                }
            }
            return result;
        }

        /**
         * This method creates a new resource and adds all properties of the
         * given XML element as a sequence of properties of this resource.
         * 
         * @param e the XML element defining the properties to add to the
         *        resource
         * @return a newly created resource with properties loaded from the
         *         given XML element
         */
        private Resource handleContentAsResource(XMLElement e) {
            // The value of the property is an intermediate resource and
            // all child XML elements define properties of this intermediate
            // resource.
            ID resourceID = getAttributeAsId(e, ATTR_RDF_RESOURCE);
            if (resourceID == null) {
                resourceID = new ID();
            }
            Resource resource = fModel.getResource(resourceID, true);
            handleChildrenOfResourceElement(e, resource);
            return resource;
        }

        /**
         * This method creates and returns a new resource corresponding to the
         * given XML node.
         * 
         * @param e the XML element defining a new resource
         * @return a new resource corresponding to the given XML node
         */
        private Resource handleResourceElement(XMLElement e) {
            ID resourceID = getResourceID(e);
            Resource resource = fModel.getResource(resourceID, true);
            ID typeID = getResourceTypeID(e);
            if (typeID != null) {
                resource.addValue(RDF_TYPE, typeID);
            }
            Set attributes = e.getAttributeNames();
            for (Iterator iterator = attributes.iterator(); iterator.hasNext();) {
                XMLName name = (XMLName) iterator.next();
                String value = e.getAttributeValue(name);
                addResourceProperty(resource, name, value);
            }
            handleChildrenOfResourceElement(e, resource);
            return resource;
        }
    }

    /**
     * This is a root handler which just traverse all children of the given XML
     * node (non-recursively), searches the corresponding IElementHandlers and
     * calls them. If for a node no specific handler was found then this method
     * loads the default element handler.
     * 
     * @author kotelnikov
     */
    class RootHandler implements IElementHandler {

        /**
         * @see adnotatio.rdf.RDFParser.IElementHandler#handle(adnotatio.common.xml.Element)
         */
        public void handle(XMLElement e) {
            int len = e.getChildNumber();
            for (int i = 0; i < len; i++) {
                XMLNode node = e.getChild(i);
                if (!(node instanceof XMLElement))
                    continue;
                XMLElement element = (XMLElement) node;
                IElementHandler handler = getHandler(element, true);
                handler.handle(element);
            }
        }
    }

    private static final XMLName ATTR_RDF_ABOUT = new XMLName(
        NS_RDF,
        null,
        "about");

    private static final XMLName ATTR_RDF_ID = new XMLName(NS_RDF, null, "id");

    private static final XMLName ATTR_RDF_PARSE_TYPE = new XMLName(
        NS_RDF,
        null,
        "parseType");

    private static final XMLName ATTR_RDF_RESOURCE = new XMLName(
        NS_RDF,
        null,
        "resource");

    private static final XMLName ELEMENT_RDF_RDF = new XMLName(
        NS_RDF,
        null,
        "RDF");

    /**
     * The default XML element handler.
     */
    private IElementHandler fDefaultHandler;

    /**
     * The default namespace for XML elements
     */
    private String fDefaultNamespace;

    /**
     * This map is used to map XML element names and corresponding element
     * handlers.
     */
    private Map fHandlers = new HashMap();

    /**
     * The datamodel to fill. All resources and properties are created using
     * this model.
     */
    private Model fModel;

    /**
     * 
     */
    public RDFParser() {
        fDefaultHandler = newDefaultHandler();
        initHandlers(fHandlers);
    }

    /**
     * Returns the default namespace
     * 
     * @return the default namespace
     */
    protected String getDefaultNamespace() {
        return fDefaultNamespace;
    }

    /**
     * Returns an element handler specific for the given element. This method
     * can return <code>null</code> if there is no specific element handlers.
     * 
     * @param element the element for which the corresponding element handler
     *        should be defined
     * @param getDefault if this flag is <code>true</code> and no
     *        element-specific handlers was found then this method will return
     *        the default handler
     * @return a handler specific for the given element
     */
    public IElementHandler getHandler(XMLElement element, boolean getDefault) {
        XMLName name = element.getNodeName();
        IElementHandler handler = (IElementHandler) fHandlers.get(name);
        if (handler == null && getDefault) {
            handler = fDefaultHandler;
        }
        return handler;
    }

    /**
     * This method is used to fill the given map with XML element handlers ({@link IElementHandler}
     * instances}. This map contains instances of the {@link XMLName} as keys.
     * 
     * @param handlers a map of handlers to fill.
     */
    protected void initHandlers(Map handlers) {
        handlers.put(ELEMENT_RDF_RDF, new ResourceHandler());
    }

    /**
     * This method creates and returns a new model with resources and their
     * properties defined in the given XML.
     * 
     * @param doc the XML document defining resources and their properties
     * @return a new model containing all resources and properties defined in
     *         the XML.
     */
    public Model load(XMLDocument doc) {
        fModel = new Model(null, new ID());
        try {
            XMLElement root = doc.getRootElement();
            IElementHandler handler = getHandler(root, true);
            handler.handle(root);
            return fModel;
        } finally {
            fModel = null;
        }
    }

    /**
     * Creates and returns a new default XML element handler.
     * 
     * @return a newly created default handler
     */
    protected IElementHandler newDefaultHandler() {
        return new ResourceHandler();
    }

    /**
     * This method transforms the name of the XML tag into an identifier and
     * returns this id.
     * 
     * @param e the XML element defining for which the corresponding ID should
     *        be returned
     * @return the identifier corresponding to the tag name
     */
    protected ID toID(XMLElement e) {
        XMLName name = e.getNodeName();
        return toID(name);
    }

    /**
     * Returns an identifier corresponding to the given XML name
     * 
     * @param name the XML name for which an identifier should be returned
     * @return an identifier corresponding to the given XML name
     */
    protected ID toID(XMLName name) {
        String ns = name.getNamespace();
        if (ns == null) {
            ns = getDefaultNamespace();
        }
        ID propertyID = new ID(ns, name.getLocalName());
        return propertyID;
    }
}